# A 4KB test suite based on method names.
#
# MicroTest has two assertions: should and should_not. They start the method
# name, after which you can put anything. A 'should' method expects true and
# a 'should_not' method expects false to be returned.
#
# MicroTest includes MicroStub, a microscopic stubber. For example:
#   Verb.stub(:meth, true) => Verb.meth == true
#   Verb.stub(:meth) { |arg1,arg2| return_value }
#
# Example MicroTest:
#
#   class BinaryMicroTest < MicroTest
#     def should_exit_cleanly
#       `ruby #{MYEXEC}`; $? == 0
#     end
#   end
#
# Microtest also includes a built-in self-test:
#
#   SELFTEST=true ruby microtest.rb
#
class MicroTest

  class MicroFailure < StandardError; end

  class << self

    def run
      init and report { execute }
    end

    def init
      @passed, @failed, @executed, @failures = 0, 0, 0, []
      @planned, @plan = micros.inject(0) { |c, (k,v)| c += v.size; c }, micros
    end

    def fp(s); print s; STDOUT.flush; end
    def r(s); "\e[1;31m#{s}\e[0m"; end
    def g(s); "\e[1;32m#{s}\e[0m"; end
    def y(s); "\e[1;33m#{s}\e[0m"; end

    def execute
      puts "Running #{@planned} test(s)..."
      @plan.each do |k, units|
        i = k.new
        units.each do |u|
          i.setup
          expected = (u.to_s =~ /^should_not/ ? false : true)
          r = catch_failures(k, u) { expect(expected) { i.send(u) } }
          r ? (@passed += 1; print g(".")) : (@failed += 1; print r("F"))
          @executed += 1
          i.teardown
          MicroStub.clear!
        end
      end
    end

    def expect(exp)
      raise MicroFailure, "should be #{exp}" unless yield == exp
    end

    def catch_failures(k, u)
      begin
        yield; true
      rescue Exception => e
        @failures << [k, u, e]; false
      end
    end

    def report
      a = Time.new; yield; z = Time.new
      puts "\n%0.1f microseconds.\n%d planned, %d executed, %d passed, %d failed.\n" %
        [(z - a) * 10**6, @planned, @executed, @passed, @failed]
      unless @failures.empty?
        puts r("\nFAILURE REPORT\n")
        @failures.each { |f| report_failure(f) }
      end
    end

    def report_failure(f)
      puts y("#{f[0]}##{f[1]}") + "\n\t#{f[2].class}\n\t#{f[2].message})"
      puts ("\t" << f[2].backtrace.join("\n\t"))
    end

    def micros
      @micros ||= {}
    end

    def method_added(symbol)
      (MicroTest.micros[self] ||= []) << symbol if symbol.to_s =~ /^should_/
    end
  end

  def setup; end
  def teardown; end

  def difference(v); a = v.dup; yield; v != a; end

end

module MicroStub

  class StubError < StandardError; end

  class << self

    def stubs
      @stubs ||= []
    end

    def save(obj, sym)
      obj.eigen.send(:alias_method, "_#{sym}_", sym) && stubs << [obj, sym]
    end

    def clear!
      stubs.each { |(obj,sym)| obj.eigen.send(:alias_method, sym, "_#{sym}_") } && stubs = []
    end

  end

  def stub(symbol, returns = true, &block)
    raise StubError, "#{self} does not respond to #{symbol}" if !respond_to?(symbol)
    MicroStub.save self, symbol
    definition = (block_given? ? block : Proc.new{returns})
    self.eigen.send :define_method, symbol, &definition
    self
  end

end

class Object
  include MicroStub
  def eigen; class << self; self; end; end
end

if ENV["SELFTEST"] == "true"

  class Verb; def self.i; "I"; end; end

  class MicroSelfTest < MicroTest

    def should_expects_true
      true
    end

    def should_not_expects_false
      false
    end

    def should_stub_with_argument
      Verb.stub(:i, "J").i == "J"
    end

    def should_stub_with_block
      Verb.stub(:i) { "K" }.i == "K"
    end

    def should_stub_with_arity
      Verb.stub(:i) { |a,b,c| "L" }.method(:i).arity == 3
    end

    def should_isolate_stubs
      Verb.i == "I"
    end

  end
end

if ENV["BENCH"] == "true"
  class BenchTest < MicroTest
    for i in 1..2500; define_method("should_#{i}") { true }; end
  end
end

at_exit do
  MicroTest.run
end
